﻿using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
using System.Threading;
using AcSdk.Data;

namespace AcSdk
{
    public class AcSdk
    {
        private Thread _thread;
        private readonly SynchronizationContext _context;

        private bool _initialized;
        private int _initWaitTime;
        private int _waitTime;

        private readonly PhysicsReader _physicsReader;
        private readonly GraphicsReader _graphicsReader;
        private readonly StaticReader _staticReader;

        /// <summary>
        /// The status of the connection
        /// </summary>
        public enum ConnectionStatuses
        {
            /// <summary>
            /// The SDK is disconnected from the sim
            /// </summary>
            Disconnected = 0,
            /// <summary>
            /// The SDK is connected to the sim and able to communicate
            /// </summary>
            Connected = 1,
            /// <summary>
            /// The SDK is waiting for the sim to start
            /// </summary>
            WaitingForSim = 2,
            /// <summary>
            /// An unknown error has occured trying to connect to the sim
            /// </summary>
            Error = 3
        }

        /// <summary>
        /// Raised when the sdk first communicates with the sim
        /// </summary>
        public event EventHandler Connected;

        /// <summary>
        /// Raised after the sdk is stopped
        /// </summary>
        public event EventHandler Disconnected;

        /// <summary>
        /// Raised once when the static info is updated.
        /// </summary>
        public event EventHandler<StaticEventArgs> StaticInfoUpdated;

        /// <summary>
        /// Raised once for every physics and graphics update.
        /// </summary>
        public event EventHandler<UpdateEventArgs> Updated;

        /// <summary>
        /// Creates a new instance of the AcSdk class.
        /// </summary>
        /// <param name="context">Optionally specify a synchronization context to change on which thread the events are raised.</param>
        public AcSdk(SynchronizationContext context = null)
        {
            _context = context ?? SynchronizationContext.Current;

            _physicsReader = new PhysicsReader();
            _graphicsReader = new GraphicsReader();
            _staticReader = new StaticReader();

            _initWaitTime = 1000;
            _connectionStatus = ConnectionStatuses.Disconnected;
            this.UpdateFrequency = 5;
        }

        private bool _running;
        /// <summary>
        /// Gets whether the update loop is currently running.
        /// </summary>
        public bool IsRunning { get { return _running; } }

        private int _updateFrequency;
        /// <summary>
        /// Gets or sets the update frequency (number of updates per second) of the update loop.
        /// </summary>
        public int UpdateFrequency
        {
            get { return _updateFrequency; }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException("UpdateFrequency must be at least 1.");
                if (value > 60)
                    throw new ArgumentOutOfRangeException("UpdateFrequency cannot be more than 60.");

                _updateFrequency = value;
                _waitTime = (int)Math.Floor(1000f / value) - 1;
            }
        }

        private ConnectionStatuses _connectionStatus;
        /// <summary>
        /// Gets the status of the connection to the sim
        /// </summary>
        public ConnectionStatuses ConnectionStatus
        {
            get { return _connectionStatus; }
        }

        private bool Initialize()
        {
            if (_initialized) return true;

            try
            {
                _physicsReader.Initialize();
                _graphicsReader.Initialize();
                _staticReader.Initialize();

                _initialized = true;
                _connectionStatus = ConnectionStatuses.Connected;
                return true;
            }
            catch (FileNotFoundException e)
            {
                // Sim is not running?
                return false;
            }
            catch (Exception ex)
            {
                _connectionStatus = ConnectionStatuses.Error;
                // Re-throw any other error
                throw;
            }
        }

        /// <summary>
        /// Starts the update loop.
        /// </summary>
        public void Start()
        {
            if (_running) return;

            if (_thread != null)
            {
                _thread.Abort();
                _thread = null;
            }

            _connectionStatus = ConnectionStatuses.WaitingForSim;
            _thread = new Thread(Loop);
            _running = true;
            _thread.Start();
        }

        /// <summary>
        /// Stops the update loop.
        /// </summary>
        public void Stop()
        {
            _running = false;
        }

        private void Loop()
        {
            // Wait until sim is running
            while (!_initialized)
            {
                var init = this.Initialize();
                if (!init) Thread.Sleep(_initWaitTime);
            }
            
            // Connected
            _connectionStatus = ConnectionStatuses.Connected;
            this.RaiseEvent(OnConnected, EventArgs.Empty);

            // Read the static data only once
            var staticData = _staticReader.Read();
            this.RaiseEvent(OnStaticInfoUpdated, new StaticEventArgs(DateTime.Now, staticData));

            // Update loop
            while (_running)
            {
                // Read the physics and graphics every iteration
                var physics = _physicsReader.Read();
                var graphics = _graphicsReader.Read();

                // Report update
                this.RaiseEvent(OnUpdated, new UpdateEventArgs(DateTime.Now, physics, graphics));

                // Wait until next update
                if (_waitTime <= 0 || _waitTime > 1000) _waitTime = 15;
                Thread.Sleep(_waitTime);
            }

            // Disconnected
            _connectionStatus = ConnectionStatuses.Disconnected;
            this.RaiseEvent(OnDisconnected, EventArgs.Empty);
        }

        /// <summary>
        /// Invokes a method (action) on the context thread, or on the default thread.
        /// </summary>
        private void RaiseEvent<T>(Action<T> action, T e)
            where T : EventArgs
        {
            var callback = new SendOrPostCallback(obj => action(obj as T));
            if (_context != null)
            {
                _context.Post(callback, e);
            }
            else
            {
                callback.Invoke(e);
            }
        }

        protected virtual void OnConnected(EventArgs e)
        {
            if (this.Connected != null)
            {
                this.Connected(this, e);
            }
        }

        protected virtual void OnDisconnected(EventArgs e)
        {
            if (this.Disconnected != null)
            {
                this.Disconnected(this, e);
            }
        }

        protected virtual void OnStaticInfoUpdated(StaticEventArgs e)
        {
            if (this.StaticInfoUpdated != null)
            {
                this.StaticInfoUpdated(this, e);
            }
        }

        protected virtual void OnUpdated(UpdateEventArgs e)
        {
            if (this.Updated != null)
            {
                this.Updated(this, e);
            }
        }

        public class StaticEventArgs : EventArgs
        {
            public StaticEventArgs(DateTime updateTime, Static data)
            {
                this.UpdateTime = updateTime;
                this.Data = data;
            }

            /// <summary>
            /// The time at which this update was reported.
            /// </summary>
            public DateTime UpdateTime { get; private set; }

            /// <summary>
            /// The static data reported by the sim.
            /// </summary>
            public Static Data { get; private set; }
        }

        public class UpdateEventArgs : EventArgs
        {
            public UpdateEventArgs(DateTime updateTime, Physics physics, Graphics graphics)
            {
                this.UpdateTime = updateTime;
                this.Physics = physics;
                this.Graphics = graphics;
            }

            /// <summary>
            /// The time at which this update was reported.
            /// </summary>
            public DateTime UpdateTime { get; private set; }

            /// <summary>
            /// The physics data reported by the sim.
            /// </summary>
            public Physics Physics { get; private set; }

            /// <summary>
            /// The graphics data reported by the sim.
            /// </summary>
            public Graphics Graphics { get; private set; }
        }

        protected abstract class DataReader<T> : IDisposable
        {
            private MemoryMappedFile _mmf;
            public MemoryMappedFile MemoryMappedFile { get { return _mmf; } }

            protected abstract string Filename { get; }

            public void Initialize()
            {
                // Open the MMF
                _mmf = MemoryMappedFile.OpenExisting(this.Filename);
            }

            public T Read()
            {
                // Open a view stream to read the MMF
                using (var stream = _mmf.CreateViewStream())
                {
                    using (var reader = new BinaryReader(stream))
                    {
                        // Read a number of bytes equal to the size of the struct
                        var size = Marshal.SizeOf(typeof(T));
                        var bytes = reader.ReadBytes(size);

                        // Pin the handle 
                        var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);

                        // Copy the data to our struct
                        var data = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));

                        // Free the handle
                        handle.Free();

                        return data;
                    }
                }
            }

            void IDisposable.Dispose()
            {
                _mmf.Dispose();
                _mmf = null;
            }
        }


        protected class PhysicsReader : DataReader<Physics>
        {
            protected override string Filename
            {
                get { return Constants.MmfFileNames.Physics; }
            }
        }

        protected class GraphicsReader : DataReader<Graphics>
        {
            protected override string Filename
            {
                get { return Constants.MmfFileNames.Graphics; }
            }
        }

        protected class StaticReader : DataReader<Static>
        {
            protected override string Filename
            {
                get { return Constants.MmfFileNames.Static; }
            }
        }
    }

}
